/*NOTICE

This software was produced for the U. S. Government
under Contract No. FA8721-09-C-0002, and is
subject to the Rights in Noncommercial Computer Software
and Noncommercial Computer Software Documentation Clause
(DFARS) 252.227-7014 (JUN 1995)

(c) 2009 The MITRE Corporation. All Rights Reserved.
*/
package org.mitre.xmltransform.uml21tospmif3;
import java.io.File;

//import org.mitre.xmltransform.spmif.ComboStep;
import org.mitre.xmltransform.spmif.Device;
import org.mitre.xmltransform.spmif.ExecutionStep;
//import org.mitre.xmltransform.spmif.ExternalReferenceStep;
import org.mitre.xmltransform.spmif.HierarchyStep;
import org.mitre.xmltransform.spmif.ReferenceStep;
import org.mitre.xmltransform.spmif.SpmifOutputDocument;
import org.mitre.xmltransform.spmif.StepState;
//import org.mitre.xmltransform.spmif.SynchronizationStep;
import org.mitre.xmltransform.transform.Transform;
import org.mitre.xmltransform.uml21.Uml21InputDocument;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

public class Uml21ToSpmif3Transform extends Transform {
	
	@Override
	public void initialize(File pInputFile, File pOutputFile) {
		mInput = new Uml21InputDocument(pInputFile);
		mOutput = new SpmifOutputDocument(pOutputFile);
	}
	
	public int setArguments(String[] pTransformArguments) {
		// any # of arguments is fine
		mTransformArguments = pTransformArguments;
		return SETARGS_NO_ERROR;		
	}

	public String getUsageMessage() {
		// should never get called since any number of transform-specific arguments is fine
		String result = super.getUsageMessage();
		result += " < sequence diagam 1 > [ < sequence diagram 2 > [...]]";
		return result;
	}

	public void transform() {
		if (generateOutput()) {
			mOutput.finishDocument();
			mOutput.writeOutput();
		}		
	}

	private boolean generateOutput() {
		try {
			mOutput.createProjectElement(mInput.getInputValue("/xmi:XMI/uml:Model/@name"));
			NodeList interactions = mInput.getInputNodes("//packagedElement[@xmi:type='uml:Interaction']");
			if (interactions == null) return true;
			Node n;
			for (int i = 0; i < interactions.getLength(); i++) {
				n = interactions.item(i);
				generatePerformanceScenariosForInteraction(n);
			}
			
			return true;
		} catch (Exception e) {
			e.printStackTrace();
			return false;
		}
	}
		
	private Device getLocation(Node pContext) {
		mInput.setVariable("lifelineId", mInput.getInputValue(pContext, "@covered"));
		String executionEngine = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Lifeline=$lifelineId]/@SAExecutionEngine");
		String schedPolicy = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Lifeline=$lifelineId]/@SASchedulingPolicy");
		if (executionEngine == null || executionEngine.length() == 0) {
			String lifelinePropertyId = mInput.getInputValue(pContext, "parent::node()/lifeline[@xmi:id=$lifelineId]/@represents");
			mInput.setVariable("lifelinePropertyId", lifelinePropertyId);
			String classId = mInput.getInputValue(pContext, "parent::node()/ownedAttribute[@xmi:type='uml:Property'][@xmi:id=$lifelinePropertyId]/@type");
			mInput.setVariable("classId", classId);
			executionEngine = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Class=$classId]/@SAExecutionEngine");
			schedPolicy = mInput.getInputValue(pContext, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SAScheduler[@base_Class=$classId]/@SAschedulingPolicy");
			if (schedPolicy.equals("RateMonotonic")) {
				schedPolicy = "RM";
			} else if (schedPolicy.equals("FixedPriority")) {
				schedPolicy = "FP";
			} else if (schedPolicy.equals("FIFO")) {
				schedPolicy = "FCFS";
			} else {
				schedPolicy = "PS";
			} // what is PS? What is IS?
		}
		if (executionEngine == null || executionEngine.length() == 0) {
			return null;
		}
		return new Device(executionEngine, Device.TYPE_PROCESSING, schedPolicy, 1);
	}
	
	private String interpretTimeInMS(String pTime) {
		String time = pTime;
		int commaIndex = time.indexOf(",");
		if (commaIndex != -1) {
			int value = Integer.parseInt(time.substring(1, commaIndex));
			int firstQuoteIndex = time.indexOf("'", commaIndex + 1);
			String units = time.substring(firstQuoteIndex + 1, time.indexOf("'", firstQuoteIndex + 1));
			if (units.equalsIgnoreCase("m") || units.equalsIgnoreCase("mins")) {
				time = "" + ((float)value*60000.0);
			} else if (units.equalsIgnoreCase("ms")) {
				time = "" + value;
			} else if (units.equalsIgnoreCase("us")) {
				time = "" + ((float)value/1000.0);
			} else {// seconds
				time = "" + ((float)value*1000.0);
			}
		}
		return time;
	}
	
	private String determineInterarrivalTime(Node pInteraction, String pMessageId) {
		String interarrivalTime = null;
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message='" + pMessageId + "']/@SAoccurrence")) {
			interarrivalTime = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/RTschedulabilityModeling_profile:SATrigger[@base_Message='" + pMessageId + "']/@SAoccurrence");
		}
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PAoccurrence")) {
			interarrivalTime = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PAoccurrence");
		}
		if (interarrivalTime != null && interarrivalTime.length() > 0) {
			interarrivalTime = interpretTimeInMS(interarrivalTime);
		} else {
			interarrivalTime = null;
		}
		return interarrivalTime;
	}
	
	private String determinePriority(Node pInteraction, String pMessageId) {
		String priority = null;
		if (mInput.evaluateBooleanInputExpression(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PApriority")) {
			priority = mInput.getInputValue(pInteraction, "ancestor::xmi:XMI/PAprofile_profile:PAopenLoad[@base_Message='" + pMessageId + "']/@PApriority");
			if (priority != null && priority.length() > 0) {
				//System.out.println("priority = " + priority);
			} else {
				priority = null;
			}
		}
		return priority;
	}
	
	private boolean shouldTransform(String pInteractionName) {
		if (mTransformArguments != null && mTransformArguments.length > 0) {
			boolean found = false;
			for (int i = 0; i < mTransformArguments.length; i++) {
				if (pInteractionName.equalsIgnoreCase(mTransformArguments[i])) {
					found = true;
					break;
				}
			}
			if (!found) {
				return false;
			}
		}
		return true;
	}
	
	private void generatePerformanceScenariosForInteraction(Node pInteraction) {
		// should we generate for this interaction?
		// check based on transform-specific run-time arguments
		String interactionName = mInput.getInputValue(pInteraction, "@name");
		if (!shouldTransform(interactionName)) return;
		
		String firstMessageId = mInput.getInputValue(pInteraction, "message[1]/@xmi:id");
		String interarrivalTime = determineInterarrivalTime(pInteraction, firstMessageId);
		String priority = determinePriority(pInteraction, firstMessageId);

		NodeList messages = mInput.getInputNodes(pInteraction, "message");

		// figure out end node for naming exec graph and location
		String execGraphName = interactionName;
		Device location = null;
		String endNodeId = mInput.getInputValue(pInteraction, 
				"message[@xmi:id='" + firstMessageId + "']/@receiveEvent");
		if (endNodeId != null && endNodeId.length() > 0) {
			Node endNode = mInput.getInputNode(pInteraction, 
					"fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + endNodeId + "']");
			execGraphName =  buildEventOccurrenceNodeName(endNode);
			location = getLocation(endNode);
		}
		
		// create perf scenario and main exec graph
		Element perfScenarioElement = mOutput.createPerformanceScenario(interactionName, mInput.getFileName(), interarrivalTime, "ms", priority);
		Element executionGraphElement = createExecutionGraph(
				perfScenarioElement, execGraphName, true);
		StepState state = new StepState(perfScenarioElement, executionGraphElement, interactionName, null, location);
		mOutput.registerLocation(location);
		
		generateExecutionGraph(	messages, 0, state);
	}
	
	protected Element createExecutionGraph(Element pParent, String pName, boolean pIsMain) {
		String id = pName;
		return mOutput.createExecutionGraph(pParent, id, pName, pIsMain);
	}

	protected String buildEventOccurrenceNodeName(Node pEventOccurrence) {
		StringBuffer result = new StringBuffer();

		mInput.setVariable("fragId", mInput.getInputValue(pEventOccurrence, "@xmi:id"));
		mInput.setVariable("modelId", mInput.getInputValue("/xmi:XMI/uml:Model/@xmi:id"));
		String messageName = mInput.getInputValue(pEventOccurrence, "parent::node()/message[@receiveEvent=$fragId]/@name");
		mInput.setVariable("lifelineId", mInput.getInputValue(pEventOccurrence, "@covered"));
		String lifelineInstanceName = mInput.getInputValue(pEventOccurrence, "parent::node()/lifeline[@xmi:id=$lifelineId]/@name");
		mInput.setVariable("lifelinePropId", mInput.getInputValue(pEventOccurrence, "parent::node()/lifeline[@xmi:id=$lifelineId]/@represents"));
		mInput.setVariable("lifelineClassId", mInput.getInputValue(pEventOccurrence, "parent::node()/ownedAttribute[@xmi:id=$lifelinePropId]/@type"));
		String lifelineClassName = mInput.getInputValue(pEventOccurrence, "ancestor::uml:Model/descendant::packagedElement[@xmi:type='uml:Class'][@xmi:id=$lifelineClassId]/@name");
		String name = lifelineClassName;
		if (lifelineClassName.length() == 0) {
			name = lifelineInstanceName;
		}
		if (messageName.length() == 0) {
			//TO DO: add stereotype support - not used by E-10A
/*			if (mInput.evaluateBooleanInputExpression(pEventOccurrence, "parent::node()/message[@xmi:id=$messageId]/uml:ModelElement.stereotype")) {
				mInput.setVariable("stereotypeId", mInput.getInputValue(pEventOccurrence, "ancestor::uml:Interaction/uml:Interaction.message/uml:Message[@xmi:id=$messageId]/uml:ModelElement.stereotype/uml:Stereotype/@xmi:idref"));
				String stereotypeName = mInput.getInputValue(pEventOccurrence, "/XMI/XMI.content/uml:Model[@xmi:id=$modelId]/uml:Namespace.ownedElement/uml:Stereotype[@xmi:id=$stereotypeId]/@name");
				result.append(stereotypeName);
			} else {
				result.append("anon");
			}*/
			result.append("_");
			result.append(name);
		} else {
			result.append(messageName);
			//TO DO: argument value support - not used by E-10A
			/*NodeList messages = mInput.getInputNodes(pEventOccurrence, "ancestor::uml:Interaction/uml:Interaction.message/uml:Message[@xmi:id=$messageId]/uml:Message.argument/uml:OpaqueExpression[string-length(@body) > 0]");
			Node n;
			for (int i = 0; i < messages.getLength(); i++) {
				n = messages.item(i);
				result.append("_");
				result.append(mInput.getInputValue(n, "@body"));
			}*/
			result.append("_");
			result.append(name);
		}
		//let's put a the node number of the uml:EventOccurrence within the context of the entire XMI 
		result.append("_");
		NodeList eventOccurrences = mInput.getInputNodes(pEventOccurrence, "parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification']");
		Node eo;
		String xmiId;
		String nodeId = mInput.getInputValue(pEventOccurrence, "@xmi:id");
		for (int i = 0; i < eventOccurrences.getLength(); i++) {
			eo = eventOccurrences.item(i);
			xmiId = mInput.getInputValue(eo, "@xmi:id");
			if (xmiId.equals(nodeId)) {
				result.append((i+1));
				break;
			}
		}
		return result.toString();
	}

	private Node getReceiveEventFragment(Node pMessage) {
		Node result = null;
		String endNodeId = mInput.getInputValue(pMessage, "@receiveEvent");
		if (endNodeId != null && endNodeId.length() > 0) {
			result = mInput.getInputNode(pMessage, 
					"parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + endNodeId + "']");
		}
		return result;
	}
	
	public Node getSendEventFragment(Node pMessage) {
		Node result = null;
		String startNodeId = mInput.getInputValue(pMessage, "@sendEvent");
		if (startNodeId != null && startNodeId.length() > 0) {
			result = mInput.getInputNode(pMessage, 
					"parent::node()/fragment[@xmi:type='uml:MessageOccurrenceSpecification'][@xmi:id='" + startNodeId + "']");
		}
		return result;		
	}
	
	// create an execution step per message
	// 
	private int generateExecutionGraph(NodeList pMessages, int pStartIndex, StepState pState) {
		Node messageNode, nextMessageNode;;
		Node receiveEventFragment, sendEventFragment, nextSendEventFragment;
		String executionLifelineId, sendLifelineId, receiveLifelineId, id, sendEventId, operationId, nextSendLifelineId;
		Device endNodeLocation;
		HierarchyStep localStep, newStep;
		String duration;
		double localDuration = 0;
		Element newExecutionGraphElement;
		String newExecutionGraphName;

		// first message is message being described
		messageNode = pMessages.item(pStartIndex);
		id = mInput.getInputValue(messageNode, "@xmi:id");
		receiveEventFragment = getReceiveEventFragment(messageNode);
		executionLifelineId = mInput.getInputValue(receiveEventFragment, "@covered"); 
		localStep = new ExecutionStep(id, null, messageNode, 
				"Local_" + buildEventOccurrenceNodeName(receiveEventFragment), mInput, mOutput);
		duration = mInput.getInputValue(messageNode, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Message='" + id + "']/@RTduration");
		if (duration != null && duration.length() > 0) {
			localDuration = Double.parseDouble(interpretTimeInMS(duration));
		}

		localStep.setDuration("" + localDuration);
		pState.addParentStep(localStep);
		if (!pState.closeParentStep(null)) {
			//System.exit(0);
		}

		// now let's go through others
		int index = pStartIndex + 1;
		while (index < pMessages.getLength()) {
			messageNode = pMessages.item(index);
			id = mInput.getInputValue(messageNode, "@xmi:id");
			receiveEventFragment = getReceiveEventFragment(messageNode);
			receiveLifelineId = mInput.getInputValue(receiveEventFragment, "@covered");
			sendEventFragment = getSendEventFragment(messageNode);
			sendLifelineId = mInput.getInputValue(sendEventFragment, "@covered");
			if (sendLifelineId.equals(executionLifelineId)) {
				duration = mInput.getInputValue(messageNode, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Message='" + id + "']/@RTduration");
				
				if (duration == null || duration.length() == 0) {
					// no duration on message, try operation
					sendEventId = mInput.getInputValue(sendEventFragment, "@event");
					operationId = mInput.getInputValue("/xmi:XMI/uml:Model//packagedElement[@xmi:type='uml:SendOperationEvent'][@xmi:id='" + sendEventId + "']/@operation");
					if (operationId != null && operationId.length() > 0) {
						duration = mInput.getInputValue(messageNode, "ancestor::xmi:XMI/RTtimeModeling_profile:RTaction[@base_Operation='" + operationId + "']/@RTduration");
					}
				}
				if (duration != null && duration.length() > 0) {
					duration = interpretTimeInMS(duration);
				} else {
					duration = null;
				}
				if (duration != null && localDuration > 0) {
					localDuration -= Double.parseDouble(duration);
				}
				
				// get location
				endNodeLocation = getLocation(receiveEventFragment);									
				mOutput.registerLocation(endNodeLocation);

				if (((index+1) < pMessages.getLength())) {
					nextMessageNode = pMessages.item(index+1);
					nextSendEventFragment = getSendEventFragment(nextMessageNode);
					nextSendLifelineId = mInput.getInputValue(nextSendEventFragment, "@covered");
				} else {
					nextSendLifelineId = null;
				}
				if (nextSendLifelineId != null 
						&& receiveLifelineId.equals(nextSendLifelineId)
						&& !receiveLifelineId.equals(sendLifelineId)) // make sure not recursive
				{
					// we create a ReferenceStep and a separate execution graph
					newExecutionGraphName = buildEventOccurrenceNodeName(receiveEventFragment);
					newStep = new ReferenceStep(id, null, messageNode, newExecutionGraphName, mInput, mOutput);

					//newExecutionGraphName = mInput.buildNodeName(endNode, null);
					newExecutionGraphElement = createExecutionGraph(pState.getPerformanceScenarioElement(), newExecutionGraphName, false); 
					index = generateExecutionGraph(	pMessages,
											index, 
											new StepState(	pState.getPerformanceScenarioElement(), newExecutionGraphElement,
															newExecutionGraphName, pState, endNodeLocation));
					
					((ReferenceStep)newStep).setEG(newExecutionGraphName, newExecutionGraphName);
				} else {
					newStep = new ExecutionStep(id, null, messageNode, buildEventOccurrenceNodeName(receiveEventFragment), mInput, mOutput);
					newStep.setLocation(endNodeLocation);
					newStep.setDuration(duration);
				}
				pState.addParentStep(newStep);
				if (!pState.closeParentStep(null)) {
				}
				pState.addToStepMap(id, newStep);				
			} else {
				localStep.setDuration("" + localDuration);
				index--;
				break; // leave the while loop
			}
			if (index >= pMessages.getLength()) {
				localStep.setDuration("" + localDuration);
			} else {
				index++;
			}
		} // while (i ...)
		
		if (!pState.isValidEndState()) {
			throw new IllegalArgumentException("Failed to close all parent steps.");
		}
		
		// start node
		if (pState.getNumberOfRootSteps() > 0) {
			HierarchyStep startStep = pState.getRootStep(0);
			pState.getExecutionGraphElement().setAttribute("StartNode", startStep.getStartNodeId());
		} else {
			pState.getExecutionGraphElement().setAttribute("StartNode", "");
		}
				
		// nodes
		HierarchyStep step;
		for (int i = 0; i < pState.getNumberOfRootSteps(); i++) {
			step = pState.getRootStep(i);
			step.generateStep(pState.getExecutionGraphElement(), pState.getLocation());
		}
		
		// arcs
		String fromNode = null;
		for (int i = 0; i < pState.getNumberOfRootSteps(); i++) {
			step = pState.getRootStep(i);
			fromNode = step.generateArcs(pState.getExecutionGraphElement(), fromNode);
		}
		return index;
	}
	
	private String[] mTransformArguments;
	private Uml21InputDocument mInput;
	private SpmifOutputDocument mOutput;
}
